﻿using System.Runtime.CompilerServices;
using Foundation.Core;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace Foundation.EntityFrameworkCore
{

    public abstract class EntityStateResetTracker : IEntityEntryTracker, IEntityStateResetTracker, IEntityTrackingDbContextSetter, IDisposable
    {
        private const string _isInnerExceptionKey = "$IsEntityTrackingException";

        protected static IReadOnlyList<EntityState> ChangedStates = new List<EntityState>() { EntityState.Added, EntityState.Modified, EntityState.Deleted }.AsReadOnly();
        protected static IReadOnlyList<EntityState> TrackingStates = new List<EntityState>() { EntityState.Unchanged, EntityState.Added, EntityState.Modified, EntityState.Deleted }.AsReadOnly();

        private readonly string _handledExceptionKey;

        public EntityStateResetTracker()
        {
            var guid = Guid.NewGuid().ToString("N").Substring(8);
            _handledExceptionKey = $"{ this.GetType().Name}_hEx_{guid}";
        }

        protected Func<IList<DbContext>> DbContextGetter { get; set; }

        protected Dictionary<object, string> TrackingEntities = new Dictionary<object, string>(5);

        public void SetDbContextGetter(Func<IList<DbContext>> getter)
        {
            DbContextGetter = getter;
        }

        public Task ResetAsync(Exception exception = null, CancellationToken cancellationToken = default)
        {
            Reset(exception);

            return Task.CompletedTask;
        }

        public void Reset(Exception exception = null)
        {
            if (CheckHandledAndSet(exception)) return;

            try
            {
                var dbContexts = GetContexts();
                if (!dbContexts.IsNullOrEmpty())
                {
                    foreach (var context in dbContexts)
                    {
                        if (CanHandleException(exception, context))
                        {
                            ResetEntry(context);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                SetInnerException(ex);
                throw;
            }
        }

        public virtual void SaveChangesFailed(SaveChangesFailedEventArgs args)
        {
            if (CheckHandledAndSet(args.Exception)) return;

            try
            {
                if (CanHandleException(args.Exception, args.DbContext, true))
                {
                    ResetEntry(args.DbContext, args.Entries);
                }
            }
            catch (Exception ex)
            {
                SetInnerException(ex);
                throw;
            }
        }

        public virtual void StateChanged(EntityStateChangedEventArgs args)
        {
            // 状态变化：修改 ---> Unchanged ,一般是实体保存到数据库之后状态的改变
            //修改 ---> Detached ， 一把是业务代码自动设置，或跟踪器设置
            if (ChangedStates.Contains(args.OldState)
                && (args.NewState == EntityState.Unchanged || args.NewState == EntityState.Detached))
            {
                if (args.NewState == EntityState.Detached)
                {
                    TrackingEntities.Remove(args.Entry.Entity);
                }

                return;
            }

            try
            {
                if (IsTracKingEntry(args.Entry))
                {
                    TrackingEntities.Add(args.Entry.Entity, args.OldState == EntityState.Detached || args.NewState == EntityState.Added ? "u" : "q");
                }
            }
            catch (Exception ex)
            {
                SetInnerException(ex);
                throw;
            }
        }

        public virtual void Tracked(EntityTrackedEventArgs args)
        {
            try
            {
                if (IsTracKingEntry(args.Entry))
                {
                    TrackingEntities.Add(args.Entry.Entity, args.FromQuery ? "q" : "u");
                }
            }
            catch (Exception ex)
            {
                SetInnerException(ex);
                throw;
            }
        }

        public void Dispose()
        {
            TrackingEntities.Clear();
        }

        protected virtual bool IsTracKingEntry(EntityEntry entry)
            => !TrackingEntities.ContainsKey(entry.Entity);

        protected virtual bool CanHandleEntry(EntityEntry entry)
            => TrackingEntities.ContainsKey(entry.Entity) && ChangedStates.Contains(entry.State);

        protected virtual bool CanHandleException(Exception exception, DbContext dbContext, bool isFromSaveChanges = false)
            => !isFromSaveChanges;

        protected virtual void ResetEntry(DbContext dbContext)
        {
            var entries = dbContext.ChangeTracker.Entries().ToArray();

            ResetEntry(dbContext, entries);
        }

        protected virtual void ResetEntry(DbContext dbContext, IList<EntityEntry> entries)
        {
            try
            {
                foreach (var entry in entries.Where(e => CanHandleEntry(e)))
                {
                    switch (entry.State)
                    {
                        case EntityState.Unchanged: ResetUnchangeEntry(entry, dbContext); break;
                        case EntityState.Deleted:
                        case EntityState.Modified:
                        case EntityState.Added: ResetStateChanged(entry, dbContext); break;
                        default: break;
                    }
                }
            }
            catch (Exception ex)
            {
                SetInnerException(ex);
                throw;
            }
        }

        protected virtual void ResetUnchangeEntry(EntityEntry entry, DbContext dbContext)
        {
            entry.State = EntityState.Detached;
        }

        /// <summary>
        /// 重置状态已修改的实体
        /// </summary>
        /// <param name="entry"></param>
        /// <param name="dbContext"></param>
        protected virtual void ResetStateChanged(EntityEntry entry, DbContext dbContext)
        {
            foreach (var property in entry.Properties.Where(p => p.IsModified))
            {
                property.CurrentValue = property.OriginalValue;
                property.IsModified = false;
            }

            entry.State = EntityState.Detached;
        }

        /// <summary>
        /// 重置保存到数据库时抛出 <see cref="DbUpdateConcurrencyException"/> 异常实体
        /// </summary>
        /// <param name="entry"></param>
        /// <param name="dbContext"></param>
        protected virtual void ResetConcurrencyExceptionEntry(EntityEntry entry, DbContext dbContext)
        {
            ResetStateChanged(entry, dbContext);
        }

        protected virtual IList<DbContext> GetContexts() => DbContextGetter?.Invoke();

        protected DbUpdateConcurrencyException GetConcurrencyException(Exception exception)
        {
            DbUpdateConcurrencyException concurrencyException = null;
            if (exception is DbUpdateConcurrencyException cEx) concurrencyException = cEx;
            else if (exception is DbConcurrencyException dEx)
            {
                concurrencyException = dEx.InnerException as DbUpdateConcurrencyException;
            }

            return concurrencyException;
        }


        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected void SetInnerException(Exception exception)
        {
            exception.Data[_isInnerExceptionKey] = 1;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected void SetHandledException(Exception exception)
        {
            exception.Data[_handledExceptionKey] = 1;
        }

        /// <summary>
        /// 判断是否已处理
        /// </summary>
        /// <param name="exception"></param>
        /// <returns></returns>
        protected bool CheckHandledAndSet(Exception exception)
        {
            if (exception == null) return false;
            if (!exception.Data.Contains(_isInnerExceptionKey)) return true;

            var handled = exception.Data.Contains(_handledExceptionKey);
            if (!handled)
            {
                SetHandledException(exception);
            }

            return handled || (exception is OperationCanceledException);
        }
    }
}
